﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQPadDriver
{
  internal partial class CommandFormatter
  {
    private readonly StringBuilder _sb = new StringBuilder();
    private readonly string _sql;
    private int _indent;

    public string Format()
    {
      try {
        int oldIndex = 0;
        while (true) {
          int newIndex = _sql.IndexOf(Environment.NewLine, oldIndex);
          if (newIndex==-1)
            newIndex = _sql.Length;
          if (Substring(oldIndex, oldIndex + 5).Equals("SELECT", StringComparison.InvariantCultureIgnoreCase))
            ParseSelect(oldIndex, newIndex - 1);
          else
            WriteLine(Substring(oldIndex, newIndex));
          oldIndex = newIndex + Environment.NewLine.Length;
          if (oldIndex >= _sql.Length)
            break;
        }
        return string.Join(
          Environment.NewLine,
          _sb.ToString().Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries).Select(
            a => a.TrimEnd()).Where(a => a!=string.Empty));
      }
      catch {
        return _sql;
      }
    }

    #region Non-public methods

    private int FindAnyOnSameLevel(int from, int to, params string[] subStrings)
    {
      return (from s in subStrings
        let i = FindOnSameLevel(s, @from, to)
        select i==-1 ? null : (int?) i).Min() ?? -1;
    }

    private int FindClosingQuote(char c, int from, int to)
    {
      for (int i = from; i <= to; i++) {
        if (_sql[i]!=c)
          continue;
        if (i==to || _sql[i + 1]!=c)
          return i;
        i++;
      }
      throw new ApplicationException();
    }

    private int FindOnSameLevel(string subString, int from, int to, bool required = false)
    {
      for (int i = from; i <= to - subString.Length + 1; i++) {
        char c = _sql[i];
        if (char.ToLower(c)==char.ToLower(subString[0])) {
          if (_sql.Substring(i, subString.Length).Equals(subString, StringComparison.InvariantCultureIgnoreCase))
            return i;
        }
        else
          switch (c) {
            case '(':
              i = FindOnSameLevel(")", i + 1, to, true);
              break;
            case '[':
              i = FindClosingQuote(']', i + 1, to);
              break;
            case '\'':
              i = FindClosingQuote('\'', i + 1, to);
              break;
          }
      }
      if (required)
        throw new ApplicationException();
      return -1;
    }

    private void ParseExpressionList(int from, int to)
    {
      int oldIndex = from;
      var columns = new List<Tuple<int, int>>();
      while (true) {
        int newIndex = FindOnSameLevel(",", oldIndex + 1, to);
        if (newIndex < 0)
          newIndex = to;
        columns.Add(Tuple.Create(oldIndex, newIndex));
        oldIndex = newIndex + 1;
        if (newIndex >= to)
          break;
      }
      _indent++;
      {
        foreach (var column in columns)
          ParseNestedSelects(column.Item1, column.Item2);
      }
      _indent--;
    }

    private void ParseNestedSelects(int from, int to)
    {
      int endSelect = from;
      while (true) {
        int selectIndex = FindOnSameLevel("(SELECT", endSelect, to);
        if (selectIndex==-1) {
          WriteLine(Substring(endSelect, to));
          break;
        }
        WriteLine(Substring(endSelect, selectIndex));
        endSelect = FindOnSameLevel(")", selectIndex + 1, to, true);
        _indent++;
        ParseSelect(selectIndex + 1, endSelect - 1);
        _indent--;
      }
    }

    private void ParseSelect(int from, int to)
    {
      if (!Substring(from, from + 5).Equals("SELECT", StringComparison.InvariantCultureIgnoreCase))
        throw new ApplicationException();

      int fromIndex = FindOnSameLevel(" FROM ", from + 7, to);
      if (fromIndex==-1)
        fromIndex = to;
      WriteLine("SELECT");
      ParseExpressionList(from + 7, fromIndex);
      if (fromIndex==to)
        return;

      WriteLine("FROM");
      int end = FindAnyOnSameLevel(
        fromIndex + 6,
        to,
        " INNER ",
        " LEFT ",
        " RIGHT ",
        " FULL ",
        " OUTER ",
        " JOIN ",
        " WHERE ",
        " GROUP BY ",
        " ORDER BY");
      if (end==-1)
        end = to;
      ParseTableNameOrNestedSelect(fromIndex + 6, end);

      while (true) {
        int joinIndex = FindOnSameLevel(" JOIN ", end, to);
        if (joinIndex < 0)
          break;
        WriteLine(Substring(end, joinIndex + 5));
        end = FindAnyOnSameLevel(
          joinIndex + 6,
          to,
          " INNER ",
          " LEFT ",
          " RIGHT ",
          " FULL ",
          " OUTER ",
          " JOIN ",
          " WHERE ",
          " GROUP BY ",
          " ORDER BY ");
        if (end==-1)
          end = to;
        ParseTableNameOrNestedSelect(joinIndex + 6, end);
      }

      int whereIndex = FindOnSameLevel(" WHERE ", end, to);
      if (whereIndex >= 0) {
        end = FindAnyOnSameLevel(whereIndex + 7, to, " GROUP BY ", " ORDER BY ");
        if (end==-1)
          end = to;
        WriteLine("WHERE");
        _indent++;
        ParseNestedSelects(whereIndex + 7, end);
        _indent--;
      }

      int groupIndex = FindOnSameLevel(" GROUP BY ", end, to);
      if (groupIndex >= 0) {
        end = FindAnyOnSameLevel(groupIndex + 10, to, " ORDER BY ");
        if (end==-1)
          end = to;
        WriteLine("GROUP BY");
        _indent++;
        ParseExpressionList(groupIndex + 10, end);
        _indent--;
      }

      int orderIndex = FindOnSameLevel(" ORDER BY ", end, to);
      if (orderIndex >= 0) {
        end = to;
        WriteLine("ORDER BY");
        ParseExpressionList(orderIndex + 10, end);
      }

      if (end < to)
        WriteLine(Substring(end + 1, to));
    }

    private void ParseTableNameOrNestedSelect(int from, int to)
    {
      if (_sql[from]=='(') {
        WriteLine("(");
        _indent++;
        int end = FindOnSameLevel(")", from + 1, to);
        ParseSelect(from + 1, end - 1);
        _indent--;
        if (end < to)
          WriteLine(Substring(end, to));
        return;
      }
      _indent++;
      WriteLine(Substring(from, to));
      _indent--;
    }

    private string Substring(int from, int to)
    {
      return _sql.Substring(from, to - from + 1);
    }

    private void Write(string s)
    {
      _sb.Append(s.Trim());
    }

    private void WriteIndent()
    {
      _sb.Append(string.Join("", Enumerable.Repeat("  ", _indent)));
    }

    private void WriteLine(string s)
    {
      WriteIndent();
      Write(s);
      WriteLineBreak();
    }

    private void WriteLineBreak()
    {
      _sb.AppendLine();
    }

    #endregion

    public CommandFormatter(string sql)
    {
      _sql = sql;
    }
  }
}